iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 8

Day 8|Windows Server 巡檢進階:Service、Uptime 與最後開機時間

  • 分享至 

  • xImage
  •  

Day 7 我們正式開始把 PowerShell 用在 Windows Server 的健康檢查。

目前已經可以取得:

CPU
Memory
Disk

例如:

SERVER01

CPU : 22% Normal
Memory : 68% Normal
Disk C: : 71% Normal
Disk D: : 91% Critical

這比單純執行:

Test-Connection

已經多了不少資訊。

但還是有一個問題:

CPU、Memory、Disk 都正常,就代表 Server 沒問題嗎?

不一定。

因為有可能:

CPU 20%
Memory 55%
Disk 60%

但 IIS 已經停止

也可能:

所有資源都正常

但 Server 10 分鐘前才剛重開機

甚至可能:

Server 已經連續運作 400 天

這些都是系統工程師巡檢時值得注意的資訊。

所以 Day 8 我們繼續增加三個項目:

Service
Last Boot Time
Uptime

最後把巡檢結果慢慢整理成:

SERVER01

CPU : Normal
Memory : Normal
Disk : Normal
Service : Warning
Uptime : 35 Days

Overall : Warning
為什麼 Service 一定要檢查?

Windows Server 上真正提供服務的,通常不是 Windows 本身,而是上面執行的各種 Service。

例如:

IIS
SQL Server
DNS
DHCP
Print Spooler
Windows Time
Backup Agent
Monitoring Agent

假設今天是一台 Web Server。

即使:

CPU 10%
Memory 40%
Disk 50%

但:

World Wide Web Publishing Service

已經停止。

對使用者來說,這台 Server 一樣是:

有問題。

所以 Server Health Check 不能只看硬體資源。

還要知道:

它應該提供的服務,現在有沒有正常運作。

先從 Get-Service 開始

前面幾天我們已經用過:

Get-Service

例如:

Get-Service |
Select-Object Name, DisplayName, Status

可以看到:

Status Name DisplayName


Running EventLog Windows Event Log
Running W32Time Windows Time
Stopped Spooler Print Spooler

如果只想查一個 Service:

Get-Service -Name W32Time

例如:

Status Name DisplayName


Running W32Time Windows Time

這已經可以拿來做最基本的 Service Check。

不要直接把所有 Stopped Service 當成異常

這裡非常重要。

如果我們寫:

Get-Service |
Where-Object Status -eq "Stopped"

通常會看到一大堆結果。

但這不代表 Server 有幾十個異常。

Windows 本身就存在很多:

Manual
Trigger Start
Disabled
按需求啟動

的 Service。

所以真正做維運時,我不建議單純:

找出所有 Stopped Service,然後全部標紅。

比較合理的做法是:

先定義這台 Server 上「必須運作」的 Service。

建立 Critical Service 清單

假設今天我們想監控:

Windows Event Log
Windows Time
Windows Management Instrumentation

對應名稱可能是:

$CriticalServices = @(
"EventLog"
"W32Time"
"Winmgmt"
)

這跟 Day 5 的 Server Array 完全一樣。

接著逐筆檢查:

foreach ($ServiceName in $CriticalServices) {

Get-Service -Name $ServiceName

}

PowerShell 就會依序檢查:

EventLog
W32Time
Winmgmt
判斷 Service 是否 Running

可以寫成:

foreach ($ServiceName in $CriticalServices) {

$Service = Get-Service `
    -Name $ServiceName `
    -ErrorAction SilentlyContinue

if ($Service.Status -eq "Running") {

    Write-Host "$ServiceName : Running"

}
else {

    Write-Host "$ServiceName : Stopped"

}

}

結果可能:

EventLog : Running
W32Time : Running
Winmgmt : Running

如果某個 Service 停止:

EventLog : Running
W32Time : Stopped
Winmgmt : Running

我們就馬上知道:

W32Time 需要進一步確認。

但如果 Service 根本不存在呢?

這又是實際環境會遇到的狀況。

例如:

Get-Service -Name MSSQLSERVER

如果這台 Server 根本沒有安裝 SQL Server,就會找不到。

所以 Service Check 最好至少分三種狀態:

Running
Stopped
NotFound

可以改成:

foreach ($ServiceName in $CriticalServices) {

$Service = Get-Service `
    -Name $ServiceName `
    -ErrorAction SilentlyContinue

if ($null -eq $Service) {

    $Status = "NotFound"

}
elseif ($Service.Status -eq "Running") {

    $Status = "Running"

}
else {

    $Status = $Service.Status.ToString()

}

Write-Host "$ServiceName : $Status"

}

這樣就比較容易判斷:

EventLog Running
W32Time Stopped
MSSQLSERVER NotFound
把 Service 結果做成 Object

跟前幾天一樣,我們不要只停在 Write-Host。

建立:

$ServiceResults = @()

接著:

foreach ($ServiceName in $CriticalServices) {

$Service = Get-Service `
    -Name $ServiceName `
    -ErrorAction SilentlyContinue

if ($null -eq $Service) {

    $Status = "NotFound"

}
else {

    $Status = $Service.Status.ToString()

}

$Result = [PSCustomObject]@{

    ComputerName = $env:COMPUTERNAME
    ServiceName  = $ServiceName
    Status       = $Status
    CheckTime    = Get-Date

}

$ServiceResults += $Result

}

最後:

$ServiceResults

可能得到:

ComputerName ServiceName Status CheckTime


SERVER01 EventLog Running 2026/09/16 05:30
SERVER01 W32Time Stopped 2026/09/16 05:30
SERVER01 Winmgmt Running 2026/09/16 05:30

這就可以繼續:

Export-Csv

做成報表。

接著看 Server 最後一次什麼時候開機

這個資訊其實很好用。

PowerShell 可以透過:

Get-CimInstance Win32_OperatingSystem

取得。

前一天我們已經用它取得 Memory。

今天同一個 Object 裡,其實還有:

LastBootUpTime

可以執行:

Get-CimInstance Win32_OperatingSystem |
Select-Object LastBootUpTime

例如:

LastBootUpTime

2026/08/15 上午 03:21:06

也可以:

$OS = Get-CimInstance Win32_OperatingSystem

$OS.LastBootUpTime

假設結果:

2026/08/15 03:21:06

這就是:

這台 Windows 最後一次開機的時間。

為什麼 Last Boot Time 值得放進巡檢?

有幾種情況很實用。

例如使用者說:

昨天 Server 好像有斷掉。

我們可以先查:

$OS.LastBootUpTime

如果結果是:

2026/09/15 02:13

至少可以知道:

這台 Server 最近確實曾經重新啟動。

另外有些 Server 可能因為:

Windows Update
Crash
Power Failure
人工 Restart
硬體問題

被重新啟動。

LastBootUpTime 可以當作後續排查的其中一個線索。

從 Last Boot Time 算 Uptime

既然知道:

現在時間

以及:

上次開機時間

兩者相減,就可以算:

Uptime。

例如:

$OS = Get-CimInstance Win32_OperatingSystem

$LastBootTime = $OS.LastBootUpTime

$Uptime = (Get-Date) - $LastBootTime

接著:

$Uptime

可能看到:

Days : 32
Hours : 4
Minutes : 21
Seconds : 18
Milliseconds : 236
Ticks : ...
TotalDays : 32.181...
TotalHours : ...

這又是一個 Object。

所以可以直接取得:

$Uptime.Days

例如:

32

代表:

Server 已經運作 32 天。

做成比較容易閱讀的格式

如果直接把整個 $Uptime 放進報表,內容會很多。

所以可以自己整理:

$UptimeText = "$($Uptime.Days) Days $($Uptime.Hours) Hours $($Uptime.Minutes) Minutes"

例如:

32 Days 4 Hours 21 Minutes

這就比較適合拿來看。

Uptime 越久越好嗎?

這裡也很容易有一個誤解:

Uptime 500 Days

看起來好像代表:

這台 Server 超級穩。

但未必。

Uptime 很久可能同時代表:

長期沒有維護
Patch 沒有套用
一直沒有 Reboot
Pending Restart
Windows Update 沒完成

所以:

Uptime 很長本身不是故障,但值得被注意。

相反地,如果:

Uptime = 15 Minutes

也不一定有問題。

可能只是剛完成:

正常維護
Windows Update
計畫性重開機

所以 Uptime 一樣屬於:

巡檢線索,而不是單獨的故障判定。

我們可以設定一個 Uptime 提醒

假設公司內部規範是:

超過 90 Days
→ Reminder

可以:

if ($Uptime.TotalDays -ge 90) {

$UptimeStatus = "Warning"

}
else {

$UptimeStatus = "Normal"

}

但這個 90 Days 只是範例。

實際環境還是要依:

公司的 Patch Policy
Windows Update Policy
維護窗口
服務需求

來決定。

另外也可以偵測「剛剛是不是重開過」

例如:

if ($Uptime.TotalHours -lt 1) {

$RecentReboot = $true

}
else {

$RecentReboot = $false

}

如果結果:

RecentReboot = True

就代表:

這台 Server 開機還不到一小時。

這在每日巡檢時也很實用。

例如本來沒有排定維護,但早上突然看到:

LastBootTime : 05:10
Uptime : 20 Minutes

那就值得去確認:

為什麼今天凌晨突然重開?

把 Uptime 做成 Object

可以整理:

$OS = Get-CimInstance Win32_OperatingSystem

$LastBootTime = $OS.LastBootUpTime
$Uptime = (Get-Date) - $LastBootTime

$BootInfo = [PSCustomObject]@{

ComputerName = $env:COMPUTERNAME

LastBootTime = $LastBootTime

UptimeDays   = $Uptime.Days

UptimeHours  = $Uptime.Hours

UptimeText   = "$($Uptime.Days) Days $($Uptime.Hours) Hours"

CheckTime    = Get-Date

}

顯示:

ComputerName : SERVER01
LastBootTime : 2026/08/15 03:21:06
UptimeDays : 32
UptimeHours : 2
UptimeText : 32 Days 2 Hours
CheckTime : 2026/09/16 05:30:00

現在我們的 Server Check 已經又多了一塊。

Day 7 + Day 8 開始整合

目前已經有:

CPU
Memory
Disk
Service
Last Boot Time
Uptime

可以把它想成:

Windows Server

├── Performance
│ ├── CPU
│ └── Memory

├── Storage
│ └── Disk

├── Application / System
│ └── Service

└── System
├── Last Boot Time
└── Uptime

這才慢慢接近真正的 Server Health Check。

開始思考 Overall Status

假設今天:

CPU Normal
Memory Normal
Disk Normal
Service Normal
Uptime Normal

那很好判斷:

Overall = Healthy

但如果:

CPU Normal
Memory Normal
Disk Warning
Service Normal
Uptime Normal

那:

Overall = Warning

如果:

CPU Normal
Memory Normal
Disk Normal
Service Critical
Uptime Normal

整體應該:

Overall = Critical
先定義簡單規則

我們可以先使用:

Critical 優先
Warning 第二
全部正常才是 Healthy

也就是:

任何一項 Critical

Overall = Critical

沒有 Critical
但有 Warning

Overall = Warning

都沒有

Overall = Healthy

這樣邏輯會比較清楚。

判斷 Critical Service

例如:

$StoppedServices = $ServiceResults |
Where-Object {
$_.Status -ne "Running"
}

接著:

if ($StoppedServices.Count -gt 0) {

$ServiceStatus = "Critical"

}
else {

$ServiceStatus = "Normal"

}

意思就是:

只要我們定義的 Critical Service 中,有任何一個不是 Running,就標記 Critical。

注意這裡之所以可以這樣做,是因為:

我們自己定義了 Critical Service 清單

而不是拿所有 Windows Service 去判斷。

Disk 也可以判斷整體狀態

假設 Day 7 已經有:

$DiskResults

其中每一筆都有:

Normal
Warning
Critical

那可以:

if ($DiskResults.Status -contains "Critical") {

$DiskOverallStatus = "Critical"

}
elseif ($DiskResults.Status -contains "Warning") {

$DiskOverallStatus = "Warning"

}
else {

$DiskOverallStatus = "Normal"

}

這裡第一次看到:

-contains

可以簡單理解為:

這組資料裡面有沒有這個值?

例如:

Normal
Normal
Critical

那:

$DiskResults.Status -contains "Critical"

就是:

True
最後再算 Overall Status

假設目前有:

$CPUStatus
$MemoryStatus
$DiskOverallStatus
$ServiceStatus
$UptimeStatus

可以先放進 Array:

$HealthStatus = @(
$CPUStatus
$MemoryStatus
$DiskOverallStatus
$ServiceStatus
$UptimeStatus
)

例如:

Normal
Normal
Warning
Normal
Normal

接著:

if ($HealthStatus -contains "Critical") {

$OverallStatus = "Critical"

}
elseif ($HealthStatus -contains "Warning") {

$OverallStatus = "Warning"

}
else {

$OverallStatus = "Healthy"

}

這個邏輯就比前一天一個一個寫:

if CPU...
if Memory...
if Disk...

容易擴充很多。

做出 Summary Object

最後可以:

$Summary = [PSCustomObject]@{

ComputerName   = $env:COMPUTERNAME
CheckTime      = Get-Date

CPUUsage       = $CPUUsage
CPUStatus      = $CPUStatus

MemoryUsage    = $MemoryUsage
MemoryStatus   = $MemoryStatus

DiskStatus     = $DiskOverallStatus

ServiceStatus  = $ServiceStatus

LastBootTime   = $LastBootTime
UptimeDays     = [math]::Round($Uptime.TotalDays, 1)
UptimeStatus   = $UptimeStatus

OverallStatus  = $OverallStatus

}

結果就可能變成:

ComputerName : SERVER01
CheckTime : 2026/09/16 05:40

CPUUsage : 23
CPUStatus : Normal

MemoryUsage : 68
MemoryStatus : Normal

DiskStatus : Warning

ServiceStatus : Normal

LastBootTime : 2026/08/15 03:21
UptimeDays : 32.1
UptimeStatus : Normal

OverallStatus : Warning

現在第一眼就可以看到:

OverallStatus : Warning

接著再去看:

到底是哪一項 Warning?

發現:

DiskStatus : Warning

那工程師就可以優先往 Disk 去查。

今天整理一份完整 Script

把 Day 8 主要內容整理起來。

這一版先專注在:

Service
Uptime
Overall Status

CPU、Memory、Disk 可以接 Day 7 已經取得的結果。

======================================

Windows Server Health Check - Day 8

======================================

$ComputerName = $env:COMPUTERNAME
$CheckTime = Get-Date

======================================

Operating System / Uptime

======================================

$OS = Get-CimInstance Win32_OperatingSystem

$LastBootTime = $OS.LastBootUpTime

$Uptime = (Get-Date) - $LastBootTime

if ($Uptime.TotalDays -ge 90) {

$UptimeStatus = "Warning"

}
else {

$UptimeStatus = "Normal"

}

======================================

Critical Services

======================================

$CriticalServices = @(
"EventLog"
"W32Time"
"Winmgmt"
)

$ServiceResults = @()

foreach ($ServiceName in $CriticalServices) {

$Service = Get-Service `
    -Name $ServiceName `
    -ErrorAction SilentlyContinue


if ($null -eq $Service) {

    $Status = "NotFound"

}
else {

    $Status = $Service.Status.ToString()

}


$ServiceResult = [PSCustomObject]@{

    ComputerName = $ComputerName
    ServiceName  = $ServiceName
    Status       = $Status
    CheckTime    = $CheckTime

}


$ServiceResults += $ServiceResult

}

======================================

Service Overall Status

======================================

$ProblemServices = $ServiceResults |
Where-Object {
$_.Status -ne "Running"
}

if ($ProblemServices.Count -gt 0) {

$ServiceStatus = "Critical"

}
else {

$ServiceStatus = "Normal"

}

======================================

Summary

======================================

$Summary = [PSCustomObject]@{

ComputerName  = $ComputerName

CheckTime     = $CheckTime

LastBootTime  = $LastBootTime

UptimeDays    = [math]::Round(
    $Uptime.TotalDays,
    1
)

UptimeStatus  = $UptimeStatus

ServiceStatus = $ServiceStatus

}

======================================

Output

======================================

Write-Host ""
Write-Host "===== Server Health Check ====="
Write-Host ""

$Summary

Write-Host ""
Write-Host "===== Critical Services ====="
Write-Host ""

$ServiceResults

執行後可能看到:

===== Server Health Check =====

ComputerName : SERVER01
CheckTime : 2026/09/16 05:45
LastBootTime : 2026/08/15 03:21
UptimeDays : 32.1
UptimeStatus : Normal
ServiceStatus : Critical

===== Critical Services =====

ServiceName Status


EventLog Running
W32Time Stopped
Winmgmt Running

現在就很清楚了:

ServiceStatus = Critical

問題來源:

W32Time = Stopped
再輸出 CSV

延續前面的做法:

$ReportFolder = "C:\Temp"
$Date = Get-Date -Format "yyyyMMdd"

確認資料夾:

if (-not (Test-Path $ReportFolder)) {

New-Item `
    -Path $ReportFolder `
    -ItemType Directory |
    Out-Null

}

Server Summary:

$Summary |
Export-Csv -Path "$ReportFolder\Server_Health_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

Service:

$ServiceResults |
Export-Csv -Path "$ReportFolder\Service_Status_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

最後:

C:\Temp

├── Server_Health_20260916.csv
└── Service_Status_20260916.csv

我們的巡檢資料又比 Day 7 更完整。

Critical Service 不應該每台 Server 都一樣

這也是今天我覺得很重要的一點。

例如:

Domain Controller

可能在意:

NTDS
DNS
Netlogon
KDC
DFSR
W32Time
IIS Server

可能在意:

W3SVC
WAS
SQL Server

可能在意:

MSSQLSERVER
SQLSERVERAGENT

所以未來不能永遠寫死:

$CriticalServices = @(
"EventLog"
"W32Time"
"Winmgmt"
)

更成熟的做法會是:

Server Role

決定要檢查哪些 Service

例如:

DC01
→ NTDS / DNS / Netlogon / KDC

WEB01
→ W3SVC / WAS

SQL01
→ MSSQLSERVER / SQLSERVERAGENT

這個觀念之後做多台 Server 巡檢時會非常重要。

我們的 Health Check 已經開始有架構了

如果從 Day 1 回頭看,到現在已經不是單純:

Get-Service

而是慢慢變成:

          Windows Server
                │
   ┌────────────┼────────────┐
   │            │            │
   ▼            ▼            ▼
  CPU         Memory        Disk
   │            │            │
   └────────────┼────────────┘
                │
             Service
                │
             Uptime
                │
                ▼
          Health Status
                │
      ┌─────────┴─────────┐
      ▼                   ▼
   Summary              Detail
      │                   │
      └─────────┬─────────┘
                ▼
               CSV

這其實就是一個很基本的:

Windows Server Health Check Tool

Day 8 小結

今天我們沒有重新做一套新的東西。

而是在 Day 7 的:

CPU
Memory
Disk

上,再加入:

Service
Last Boot Time
Uptime

並開始建立:

Overall Status

其中幾個值得記住的指令與觀念:

Get-Service

Get-CimInstance Win32_OperatingSystem

$OS.LastBootUpTime

(Get-Date) - $LastBootTime

$Uptime.TotalDays

以及:

-contains

幫助我們判斷一組 Status 裡,有沒有:

Warning
Critical

但今天真正重要的還是:

不要只問「Server 有沒有活著」,而是要問「它應該提供的功能是否正常」。

所以健康檢查應該逐漸從:

Ping

進化成:

Connectivity
+
Resource
+
Storage
+
Service
+
System State

我們的巡檢 Script 到這裡,已經開始比較接近真正工作中可以使用的工具。

Day 9 預告
Day 9|Windows Event Log 自動巡檢:不要等使用者報錯才去翻事件檢視器

目前我們可以知道:

CPU 高不高?
Memory 夠不夠?
Disk 快滿了嗎?
Service 有沒有停止?
Server 什麼時候開機?

但真正遇到 Windows 問題時,我們通常還會打開:

Event Viewer

去找:

Error
Warning
Critical

所以 Day 9 我們會開始使用:

Get-WinEvent

把原本:

開 Event Viewer

點 Windows Logs

點 System

Filter Current Log

找 Error

改成:

PowerShell

自動讀 Event Log

篩選最近 24 小時

找 Critical / Error

整理 Event ID

輸出報表

下一篇我們會讓這支 Health Check 開始具備一點真正的 異常排查能力。


上一篇
Day 7|Windows Server 健康檢查:CPU、Memory、Disk 一次抓出來
下一篇
Day 9|Windows Event Log 自動巡檢:不要等使用者報錯才去翻事件檢視器
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言